using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using DslCrossModelFramework.ModelRelationship;
using DslCrossModelFramework.ModelTraversal;
using DslCrossModelFramework.NameResolution;
using Microsoft.VisualStudio.Modeling;

namespace DslCrossModelFramework.UI {
    public partial class ModelElementTree : UserControl {
        private CrossModelManager manager;
        private RelationshipDefinitionDelegate relationshipDelegate;
        private string scope;
        private int nodeCount = 0;
        private Dictionary<int, ModelPath> modelPaths = new Dictionary<int, ModelPath>();
        private Dictionary<int, IModelFileInfo> modelFiles = new Dictionary<int, IModelFileInfo>();
        private Dictionary<int, ModelBrowserIcon> customIcons = new Dictionary<int, ModelBrowserIcon>();
        private ModelSelectionMode mode;

        private ModelPath startingPath;
        private ModelElement rootElement;
        private bool hideRoot = false;
        private ModelPath selectedPath;
        private bool allowSelectNone = true;
        private List<Guid> excludedStores = new List<Guid>();
        

        #region Public Properties

        /// <summary>
        /// Gets or sets the relationship delegate.
        /// </summary>
        /// <value>The relationship delegate.</value>
        public RelationshipDefinitionDelegate RelationshipDelegate {
            get { return relationshipDelegate;  }
            set { relationshipDelegate = value; }
        }

        /// <summary>
        /// Gets or sets the scope.
        /// </summary>
        /// <value>The scope.</value>
        public string Scope {
            get { return scope; }
            set { scope = value; }
        }

        /// <summary>
        /// Gets or sets the starting path.
        /// </summary>
        /// <value>The starting path.</value>
        public ModelPath StartingPath {
            get { return startingPath; }
            set { startingPath = value; }
        }

        /// <summary>
        /// Gets or sets the root element.
        /// </summary>
        /// <value>The root element.</value>
        public ModelElement RootElement {
            get { return rootElement; }
            set { rootElement = value; }
        }

        public bool HideRoot {
            get { return hideRoot; }
            set{ hideRoot = value; }
        }

        /// <summary>
        /// Gets the selected path.
        /// </summary>
        /// <value>The selected path.</value>
        public ModelPath SelectedPath {
            get { return selectedPath; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to allow 'select none'.
        /// </summary>
        /// <value><c>true</c> if allow 'select none'; otherwise, <c>false</c>.</value>
        public bool AllowSelectNone {
            get { return allowSelectNone; }
            set { allowSelectNone = value; }
        }

        /// <summary>
        /// Gets or sets the store ids to exclude. 
        /// This can be used to exclude the current store for a cross model reference
        /// </summary>
        /// <value>The excluded store ids.</value>
        public List<Guid> ExcludedStores {
            get { return excludedStores;  }
            set { excludedStores = value; }
        }

        #endregion

        private bool isSelectingInitial = false;

        #region Constructors

        public ModelElementTree(CrossModelManager manager, RelationshipDefinitionDelegate relationshipDelegate) : this(manager, relationshipDelegate, string.Empty, ModelSelectionMode.ModelPath, null, null) { }

        public ModelElementTree(CrossModelManager manager, RelationshipDefinitionDelegate relationshipDelegate, string scope, ModelSelectionMode mode) : this(manager, relationshipDelegate, scope, mode, null, null) {}

        public ModelElementTree(CrossModelManager manager, RelationshipDefinitionDelegate relationshipDelegate, string scope, ModelSelectionMode mode, ModelPath startingPath)
            : this(manager, relationshipDelegate, scope, mode, startingPath, null) {
        }

        public ModelElementTree(CrossModelManager manager, RelationshipDefinitionDelegate relationshipDelegate, string scope, ModelSelectionMode mode, ModelElement rootElement) : this(manager, relationshipDelegate, scope, mode, null, rootElement) { }

        public ModelElementTree(CrossModelManager manager, RelationshipDefinitionDelegate relationshipDelegate, string scope, ModelSelectionMode mode, ModelPath startingPath, ModelElement rootElement) {
            InitializeComponent();
            this.manager = manager;
            this.relationshipDelegate = relationshipDelegate;
            this.scope = scope;
            this.mode = mode;
            this.startingPath = startingPath;
            this.rootElement = rootElement;
        }

        public void Bind() {
            CreateImageList();
            BindToModel();
            SelectStartingPath();

            selectNoneButton.Visible = allowSelectNone;

        }

        #endregion

        #region Images

        private enum TreeViewImages {
            dot = 0,
            openFolder,
            closedFolder,
            model,
            genericProject,
            solution,
            component
        }

        private void CreateImageList() {
            ImageList imageList = new ImageList();
            foreach(string name in Enum.GetNames(typeof(TreeViewImages))) {
                imageList.Images.Add(
                    Image.FromStream(
                        GetType().Assembly.GetManifestResourceStream(
                            string.Format("{0}.Resources.{1}.bmp", GetType().Namespace, name))));
            }

            treeView.ImageList = imageList;
        }

        #endregion

        #region Initial Selection

        private void SelectStartingPath() {
            try {
                isSelectingInitial = true;
                if (startingPath != null && startingPath.Count > 0) {
                    // Get the starting treenode to search under
                    TreeNode rootNode = null;
                    if(rootElement != null) {
                        // Get the model file tree node of the first path node in the starting path
                        TreeNode fileNode = null;
                        foreach (KeyValuePair<int, IModelFileInfo> file in modelFiles) {
                            if (file.Value.ModelFile.Equals(
                                startingPath.First.Value.ModelFileInfo.ModelFile)) {
                                fileNode = FindNodeByTagValue(file.Key);
                                if (fileNode != null) {
                                    break;
                                }
                            }
                        }
                        rootNode = fileNode;
                    }
                    else {
                        if(!hideRoot && treeView.Nodes.Count == 1) {
                            rootNode = treeView.Nodes[0];
                        }
                        else if(hideRoot) {
                            foreach(TreeNode node in treeView.Nodes) {
                                if (node.Tag != null && modelPaths.ContainsKey((int)node.Tag) && startingPath.StartsWith(modelPaths[(int)node.Tag])) {
                                    rootNode = node;
                                    break;
                                }
                            }
                        }
                    }
                    if(rootNode != null) {
                        LoadChildren(rootNode);
                        if (mode == ModelSelectionMode.ModelElement) {
                            SelectStartingPathByElement(rootNode);
                        }
                        else {
                            SelectStartingPath(rootNode);
                        }
                        selectedPath = startingPath;
                    }
                }
                isSelectingInitial = false;
            }
            catch(Exception ex) {
                throw ex;
            }
        }

        private bool SelectStartingPathByElement(TreeNode parentNode) {
            if(parentNode.Level > 15) {
                return true;
            }
            LoadChildren(parentNode);
            foreach(TreeNode currentNode in parentNode.Nodes) {
                if (currentNode.Tag != null && modelPaths.ContainsKey((int)currentNode.Tag) && modelPaths[((int)currentNode.Tag)].Last.Value.GetSerializedString() == startingPath.First.Value.GetSerializedString()) {
                    treeView.SelectedNode = currentNode;
                    treeView.Focus();
                    
                    ExpandAllAncestors(currentNode);
                    return true;
                }
                if (SelectStartingPath(currentNode)) {
                    return true;
                }
            }

            return false;
        }

        private bool SelectStartingPath(TreeNode parentNode) {
            if (parentNode.Level > 15) {
                return true;
            }
            LoadChildren(parentNode);

            foreach (TreeNode currentNode in parentNode.Nodes) {
                if (currentNode.Tag != null && modelPaths.ContainsKey((int)currentNode.Tag) && startingPath.StartsWith(modelPaths[((int)currentNode.Tag)])) {
                    if (startingPath.Count == modelPaths[((int)currentNode.Tag)].Count) {
                        treeView.SelectedNode = currentNode;
                        treeView.Focus();
                        ExpandAllAncestors(currentNode);
                        return true;
                    }
                    else if (startingPath.Count < modelPaths[((int)currentNode.Tag)].Count) {   
                        return false;
                    }
                    else {
                        return SelectStartingPath(currentNode);
                    }
                }
            }

            return false;
        }

        private void ExpandAllAncestors(TreeNode targetNode) {
            TreeNode parentNode = targetNode.Parent;
            while(parentNode != null) {
                parentNode.Expand();
                parentNode = parentNode.Parent;
            }
        }

        #endregion

        private TreeNode FindNodeByTagValue(int tagValue) {
            foreach(TreeNode treeNode in treeView.Nodes) {
                TreeNode node = FindNodeByTagValue(tagValue, treeNode);
                if(node != null) {
                    return node;
                }
            }
            return null;
        }

        private TreeNode FindNodeByTagValue(int tagValue, TreeNode parentNode) {
            if(parentNode.Tag != null && (int)parentNode.Tag == tagValue) {
                return parentNode;
            }
            foreach(TreeNode subNode in parentNode.Nodes){
                TreeNode node = FindNodeByTagValue(tagValue, subNode);
                if(node != null) {
                    return node;
                }
            }
            return null;
        }

        private void BindToModel() {
            nodeCount = 0;
            modelPaths.Clear();
            treeView.Nodes.Clear();
            TreeNode rootNode = new TreeNode();
            if (rootElement == null) {
                rootNode.Text = manager.GetModelDirectory().CurrentSolutionName;
                rootNode.SelectedImageIndex = rootNode.ImageIndex = Convert.ToInt32(TreeViewImages.solution);
                RegisterNodeTag(rootNode);
                Dictionary<string, List<IModelFileInfo>> containers = new Dictionary<string, List<IModelFileInfo>>();
                foreach (IModelFileInfo file in manager.GetModelDirectory().ModelFiles) {
                    if(file.Store != null && excludedStores.Contains(file.Store.Id)) {
                        continue;
                    }
                    if (!containers.ContainsKey(file.ModelFile.OwningAssembly)) {
                        containers.Add(file.ModelFile.OwningAssembly, new List<IModelFileInfo>());
                    }
                    containers[file.ModelFile.OwningAssembly].Add(file);
                }

                foreach (KeyValuePair<string, List<IModelFileInfo>> pair in containers) {
                    TreeNode currentOwningAssemblyNode = new TreeNode(pair.Key);
                    RegisterNodeTag(currentOwningAssemblyNode);
                    treeView.ImageList.Images.Add(pair.Value[0].ModelFile.GetContainerIcon());
                    currentOwningAssemblyNode.SelectedImageIndex =
                        currentOwningAssemblyNode.ImageIndex = treeView.ImageList.Images.Count - 1;
                    foreach (IModelFileInfo info in pair.Value) {
                        TreeNode currentModelFileNode = new TreeNode(info.ModelFile.Name);
                        currentModelFileNode.SelectedImageIndex =
                            currentModelFileNode.ImageIndex = Convert.ToInt32(TreeViewImages.model);
                        RegisterNodeTag(currentModelFileNode);
                        currentModelFileNode.Nodes.Add(new TreeNode("Loading Model..."));
                        currentOwningAssemblyNode.Nodes.Add(currentModelFileNode);
                        RegisterNodeModelFile(currentModelFileNode, info);
                    }
                    rootNode.Nodes.Add(currentOwningAssemblyNode);
                }
            }
            else {

                RegisterNodeTag(rootNode);
                ModelPath rootNodePath = RegisterNodePath(rootNode, rootElement);
                IModelElementNameResolver resolver = NameResolution.NameResolutionManager.Current.GetResolver(rootElement.GetDomainClass(), this.scope);
                rootNode.Text = resolver.GetName(rootNodePath.Last);
                rootNode.SelectedImageIndex = rootNode.ImageIndex = GetImageIndex(rootNodePath.Last, resolver);
                BindChildren(rootNodePath, rootNode, 2);

            }
            if(!hideRoot) {
                treeView.Nodes.Add(rootNode);
            }
            else {
                foreach (TreeNode node in rootNode.Nodes) {
                    treeView.Nodes.Add(node);
                }
            }
        }

        private void BindChildren(ModelPath parentPath, TreeNode currentModelNode, int recursionCount) {
            currentModelNode.Nodes.Clear();
            if(recursionCount <= 0 && !isSelectingInitial) {
                return;
            }
            foreach (IModelRelationshipDefinition definition in relationshipDelegate(parentPath.TargetModelElement.GetDomainClass(), this.scope)) {
                if(manager.GetModelDirectory().GetModelElementRoot(parentPath.TargetModelElement).GetDomainClass().ImplementationClass == definition.ModelRootType) {
                    if (parentPath.TargetModelElement.GetType() == definition.ParentType || parentPath.TargetModelElement.GetType().IsSubclassOf(definition.ParentType)) {
                        IEnumerable<ModelElement> children = definition.GetChildModelElements(parentPath, manager, scope);
                        children = new CanReferenceModelElementFilter().FilterCollection(children);
                        foreach (ModelElement element in children) {
                            if(element == null) {
                                continue;
                            }
                            TreeNode currentElementNode = new TreeNode();
                            currentElementNode.SelectedImageIndex = currentElementNode.ImageIndex = Convert.ToInt32(TreeViewImages.dot);
                            RegisterNodeTag(currentElementNode);
                            currentModelNode.Nodes.Add(currentElementNode);
                            ModelPath elementPath = RegisterNodePath(currentElementNode, element);
                            IModelElementNameResolver resolver =
                                NameResolutionManager.Current.GetResolver(element, this.scope);
                            currentElementNode.Text = resolver.GetName(elementPath.Last);
                            currentElementNode.SelectedImageIndex = currentElementNode.ImageIndex = GetImageIndex(elementPath.Last, resolver);
                            // TODO: Lazy loading of model elements
                            BindChildren(elementPath, currentElementNode, recursionCount - 1);

                        }
                        break;
                    }
                }
            }
        }

        private int GetImageIndex(LinkedListNode<IModelPathSegment> pathNode, IModelElementNameResolver resolver) {
            int index = Convert.ToInt32(TreeViewImages.dot);
            ModelBrowserIcon icon = resolver.GetIcon(pathNode);
            bool found = false;
            foreach (KeyValuePair<int, ModelBrowserIcon> pair in customIcons) {
                if(pair.Value.Name == icon.Name) {
                    index = pair.Key;
                    found = true;
                    break;
                }
            }
            if(!found) {
                index = treeView.ImageList.Images.Count;
                customIcons.Add(treeView.ImageList.Images.Count, icon);
                treeView.ImageList.Images.Add(icon.Image);
            }
            return index;
        }

        private void treeView_BeforeExpand(object sender, TreeViewCancelEventArgs e) {

        }

        private void treeView_AfterExpand(object sender, TreeViewEventArgs e) {
            LoadChildren(e.Node);
        }

        private void LoadChildren(TreeNode parentTreeNode) {
            if(parentTreeNode.Tag == null || parentTreeNode.Tag.GetType() != typeof(int)) {
                return;
            }
            if (modelFiles.ContainsKey((int)parentTreeNode.Tag)) {
                string modelFileName = parentTreeNode.Text;
                string owningAssemblyName = parentTreeNode.Parent.Text;
                if (parentTreeNode.Nodes.Count == 1 && parentTreeNode.Nodes[0].Text == "Loading Model...") {
                    // Model has not already been loaded in
                    foreach (IModelFileInfo file in manager.GetModelDirectory().ModelFiles) {
                        if (file.ModelFile.Name == modelFileName && file.ModelFile.OwningAssembly == owningAssemblyName) {
                            ModelElement modelRoot = file.Load();
                            parentTreeNode.Nodes.Clear();
                            BindChildren(new ModelPath(modelRoot), parentTreeNode, 2);
                            break;
                        }
                    }
                }
            }
            else if (modelPaths.ContainsKey((int)parentTreeNode.Tag)) {
                // The parent of this is the modelfile
                ModelElement parentModelElement = modelPaths[(int) parentTreeNode.Tag].TargetModelElement;
                BindChildren(modelPaths[(int)parentTreeNode.Tag], parentTreeNode, 2);
                
            }

        }

        private void RegisterNodeTag(TreeNode node) {
            node.Tag = nodeCount;
            nodeCount++;
        }

        private void RegisterNodeModelFile(TreeNode node, IModelFileInfo modelFileInfo) {
            int pathKey = (int)node.Tag;
            modelFiles.Add(pathKey, modelFileInfo);
        }

        private ModelPath RegisterNodePath(TreeNode node, ModelElement modelElement) {
            int pathKey = (int)node.Tag;
            int previousPathKey = node.Parent == null || node.Parent.Tag == null ? -1 : (int)node.Parent.Tag;
            ModelPath previousPath = modelPaths.ContainsKey(previousPathKey) ? modelPaths[previousPathKey] : null;
            ModelPath newModelPath = new ModelPath();
            if(previousPath == null) {
                // No parent path so this is the first
                newModelPath.Add(modelElement);
            }
            else {
                newModelPath = previousPath.Clone() as ModelPath;
                newModelPath.Add(modelElement);
                
            }
            modelPaths.Add(pathKey, newModelPath);
            return newModelPath;
        }

        public event EventHandler<ModelItemSelectionEventArgs> SelectionChanged;

        public event EventHandler<EventArgs> SelectNoneClicked;

        private void treeView_AfterSelect(object sender, TreeViewEventArgs e) {
            
            if(!this.isSelectingInitial && e.Action != TreeViewAction.Unknown) {
                if (e.Node != null && e.Node.Tag != null && modelPaths.ContainsKey((int)e.Node.Tag)) {
                    // ModelElement selected
                    if (SelectionChanged != null) { SelectionChanged(this, new ModelItemSelectionEventArgs(modelPaths[(int)e.Node.Tag])); }
                }
                else {
                    if (SelectionChanged != null) { SelectionChanged(this, new ModelItemSelectionEventArgs(null)); }
                }
            }
        }

        private void selectNoneButton_Click(object sender, EventArgs e) {
            if (SelectNoneClicked != null) { SelectNoneClicked(sender, e); }
        }

    }

    /// <summary>
    /// Event Args for Model Item Selection in a model element picker
    /// </summary>
    public class ModelItemSelectionEventArgs : EventArgs {

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelItemSelectionEventArgs"/> class.
        /// </summary>
        /// <param name="modelPath">The model path.</param>
        public ModelItemSelectionEventArgs(ModelPath modelPath) {
            this.modelPath = modelPath;
        }

        /// <summary>
        /// Gets a value indicating whether the instance selected is a model element.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if the instance selected is a model element; otherwise, <c>false</c>.
        /// </value>
        public bool IsModelElementSelected {
            get { return modelPath != null; }
        }

        private ModelPath modelPath;

        /// <summary>
        /// Gets the selected model path.
        /// </summary>
        /// <value>The selected model path.</value>
        public ModelPath SelectedModelPath {
            get { return modelPath; }
        }

        /// <summary>
        /// Gets the selected model element.
        /// </summary>
        /// <value>The selected model element.</value>
        public ModelElement SelectedModelElement {
            get { return modelPath.TargetModelElement; }
        }
    }

    /// <summary>
    /// Indicates the mode in which a model element picker will select nodes
    /// </summary>
    public enum ModelSelectionMode {

        /// <summary>
        /// Return only the selected model element
        /// </summary>
        ModelElement,

        /// <summary>
        /// Return the path taken to the selected model element
        /// </summary>
        ModelPath
    }
}
